package com.sample.normalweatherwidget;

import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.graphics.Camera;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import java.util.ArrayDeque;
import java.util.BitSet;
import java.util.Deque;
import java.util.Map;

import static android.graphics.Paint.ANTI_ALIAS_FLAG;
import static android.graphics.Paint.Style.STROKE;
import static android.view.MotionEvent.ACTION_DOWN;

/**
 * Renders your view hierarchy as an interactive 3D visualization of layers.
 * <p>
 * Interactions supported:
 * <ul>
 * <li>Single touch: controls the rotation of the model.</li>
 * <li>Two finger vertical pinch: Adjust zoom.</li>
 * <li>Two finger horizontal pinch: Adjust layer spacing.</li>
 * </ul>
 */
public class ScalpelFrameLayout extends FrameLayout {
    private static final int ROTATION_MAX = 40;
    private static final int ROTATION_DEFAULT_X = 0;
    private static final int ROTATION_DEFAULT_Y = 0;
    private static final float ZOOM_DEFAULT = 1.0f;
    private static final int SPACING_DEFAULT = 25;
    private static final int CHROME_COLOR = 0xFF888888;
    private static final int CHROME_SHADOW_COLOR = 0xFF000000;
    private static final int TEXT_OFFSET_DP = 2;
    private static final int TEXT_SIZE_DP = 10;
    private static final int CHILD_COUNT_ESTIMATION = 25;

    private Map<Integer, Integer> layerMap;
    private ValueAnimator animator;
    private float touchX, touchY; // 点击的坐标

    private static class LayeredView {
        View view;
        int layer;

        void set(View view, int layer) {
            this.view = view;
            this.layer = layer;
        }

        void clear() {
            view = null;
            layer = -1;
        }
    }

    private final Rect viewBoundsRect = new Rect();
    private final Paint viewBorderPaint = new Paint(ANTI_ALIAS_FLAG);
    private final Camera camera = new Camera();
    private final Matrix matrix = new Matrix();
    private final int[] location = new int[2];
    private final BitSet visibilities = new BitSet(CHILD_COUNT_ESTIMATION);
    private final SparseArray<String> idNames = new SparseArray<>();
    private final Deque<LayeredView> layeredViewQueue = new ArrayDeque<>();
    private final Pool<LayeredView> layeredViewPool = new Pool<LayeredView>(CHILD_COUNT_ESTIMATION) {
        @Override
        protected LayeredView newObject() {
            return new LayeredView();
        }
    };

    private final Resources res;
    private final float density;
    private final float textOffset;
    private final float textSize;

    private boolean enabled;
    private boolean drawViews = true;
    private boolean drawIds;

    private float rotationY = ROTATION_DEFAULT_Y;
    private float rotationX = ROTATION_DEFAULT_X;
    private float zoom = ZOOM_DEFAULT;
    private float spacing = SPACING_DEFAULT;

    private int chromeColor;
    private int chromeShadowColor;

    public ScalpelFrameLayout(Context context) {
        this(context, null);
    }

    public ScalpelFrameLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ScalpelFrameLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        res = context.getResources();
        density = context.getResources().getDisplayMetrics().density;

        textSize = TEXT_SIZE_DP * density;
        textOffset = TEXT_OFFSET_DP * density;

        setChromeColor(CHROME_COLOR);
        viewBorderPaint.setStyle(STROKE);
        viewBorderPaint.setTextSize(textSize);
        setChromeShadowColor(CHROME_SHADOW_COLOR);

        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                if (animator != null) {
                    animator.cancel();
                }

                int halfWidth = getWidth() / 2;
                int halfHeight = getHeight() / 2;
                touchX -= halfWidth;
                touchY -= halfHeight;

                float xPercentTemp = touchX / (Math.abs(touchX) + Math.abs(touchY));
                float yPercentTemp = -touchY / (Math.abs(touchX) + Math.abs(touchY));

                float len = (float) Math.sqrt(touchX * touchX + touchY * touchY);
                float delta = len / (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);

                final float xPercent = xPercentTemp * delta;
                final float yPercent = yPercentTemp * delta;

                animator = ValueAnimator.ofFloat(0, 1);
                animator.setDuration(1000);
                animator.setInterpolator(new TimeInterpolator() {
                    @Override
                    public float getInterpolation(float t) {
                        t -= 1.0f;
                        return (float) (Math.sin(2 * 1.5f * Math.PI * t)) * t * 1.5f;
                    }
                });
                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator valueAnimator) {
                        Float value = (Float) valueAnimator.getAnimatedValue();
                        rotationX = value * ROTATION_MAX * yPercent;
                        rotationY = value * ROTATION_MAX * xPercent;
                        invalidate();
                    }
                });
                animator.start();
            }
        });
    }

    /**
     * Set the view border chrome color.
     */
    public void setChromeColor(int color) {
        if (chromeColor != color) {
            viewBorderPaint.setColor(color);
            chromeColor = color;
            invalidate();
        }
    }

    /**
     * Get the view border chrome color.
     */
    public int getChromeColor() {
        return chromeColor;
    }

    /**
     * Set the view border chrome shadow color.
     */
    public void setChromeShadowColor(int color) {
        if (chromeShadowColor != color) {
            viewBorderPaint.setShadowLayer(1, -1, 1, color);
            chromeShadowColor = color;
            invalidate();
        }
    }

    /**
     * Get the view border chrome shadow color.
     */
    public int getChromeShadowColor() {
        return chromeShadowColor;
    }

    /**
     * Set whether or not the 3D view layer interaction is enabled.
     */
    public void setLayerInteractionEnabled(boolean enabled) {
        if (this.enabled != enabled) {
            this.enabled = enabled;
            setWillNotDraw(!enabled);
            invalidate();
        }
    }

    /**
     * Returns true when 3D view layer interaction is enabled.
     */
    public boolean isLayerInteractionEnabled() {
        return enabled;
    }

    /**
     * Set whether the view layers draw their contents. When false, only wireframes are shown.
     */
    public void setDrawViews(boolean drawViews) {
        if (this.drawViews != drawViews) {
            this.drawViews = drawViews;
            invalidate();
        }
    }

    /**
     * Returns true when view layers draw their contents.
     */
    public boolean isDrawingViews() {
        return drawViews;
    }

    /**
     * Set whether the view layers draw their IDs.
     */
    public void setDrawIds(boolean drawIds) {
        if (this.drawIds != drawIds) {
            this.drawIds = drawIds;
            invalidate();
        }
    }

    /**
     * Returns true when view layers draw their IDs.
     */
    public boolean isDrawingIds() {
        return drawIds;
    }

    public void setLayerMap(Map<Integer, Integer> map) {
        layerMap = map;
    }

    private int getLayerById(int id) {
        if (layerMap == null || id == View.NO_ID) {
            return 0;
        }
        Integer layer = layerMap.get(id);
        if (layer == null) {
            return 0;
        } else {
            return layer;
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return enabled || super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(@SuppressWarnings("NullableProblems") MotionEvent event) {
        if (event.getAction() == ACTION_DOWN) {
            touchX = event.getX();
            touchY = event.getY();
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void draw(@SuppressWarnings("NullableProblems") Canvas canvas) {
        if (!enabled) {
            super.draw(canvas);
            return;
        }

        getLocationInWindow(location);
        float x = location[0];
        float y = location[1];

        int saveCount = canvas.save();

        float cx = getWidth() / 2f;
        float cy = getHeight() / 2f;

        camera.save();
        camera.rotate(rotationX, rotationY, 0);
        camera.getMatrix(matrix);
        camera.restore();

        matrix.preTranslate(-cx, -cy);
        matrix.postTranslate(cx, cy);
        canvas.concat(matrix);
        canvas.scale(zoom, zoom, cx, cy);

        if (!layeredViewQueue.isEmpty()) {
            throw new AssertionError("View queue is not empty.");
        }

        // We don't want to be rendered so seed the queue with our children.
        for (int i = 0, count = getChildCount(); i < count; i++) {
            LayeredView layeredView = layeredViewPool.obtain();
            View child = getChildAt(i);
            layeredView.set(child, getLayerById(child.getId()));
            layeredViewQueue.add(layeredView);
        }

        while (!layeredViewQueue.isEmpty()) {
            LayeredView layeredView = layeredViewQueue.removeFirst();
            View view = layeredView.view;
            int layer = layeredView.layer;

            // Restore the object to the pool for use later.
            layeredView.clear();
            layeredViewPool.restore(layeredView);

            // Hide any visible children.
            if (view instanceof ViewGroup) {
                ViewGroup viewGroup = (ViewGroup) view;
                visibilities.clear();
                for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
                    View child = viewGroup.getChildAt(i);
                    //noinspection ConstantConditions
                    if (child.getVisibility() == VISIBLE) {
                        visibilities.set(i);
                        child.setVisibility(INVISIBLE);
                    }
                }
            }

            int viewSaveCount = canvas.save();

            // Scale the layer index translation by the rotation amount.
            float translateShowX = rotationY / ROTATION_MAX;
            float translateShowY = rotationX / ROTATION_MAX;
            float tx = layer * spacing * density * translateShowX;
            float ty = layer * spacing * density * translateShowY;
            canvas.translate(tx, -ty);

            view.getLocationInWindow(location);
            canvas.translate(location[0] - x, location[1] - y);

            viewBoundsRect.set(0, 0, view.getWidth(), view.getHeight());
            canvas.drawRect(viewBoundsRect, viewBorderPaint);

            if (drawViews) {
                view.draw(canvas);
            }

            if (drawIds) {
                int id = view.getId();
                if (id != NO_ID) {
                    canvas.drawText(nameForId(id), textOffset, textSize, viewBorderPaint);
                }
            }

            canvas.restoreToCount(viewSaveCount);

            // Restore any hidden children and queue them for later drawing.
            if (view instanceof ViewGroup) {
                ViewGroup viewGroup = (ViewGroup) view;
                for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
                    if (visibilities.get(i)) {
                        View child = viewGroup.getChildAt(i);
                        //noinspection ConstantConditions
                        child.setVisibility(VISIBLE);
                        LayeredView childLayeredView = layeredViewPool.obtain();
                        childLayeredView.set(child, getLayerById(child.getId()));
                        layeredViewQueue.add(childLayeredView);
                    }
                }
            }
        }

        canvas.restoreToCount(saveCount);
    }

    private String nameForId(int id) {
        String name = idNames.get(id);
        if (name == null) {
            try {
                name = res.getResourceEntryName(id);
            } catch (NotFoundException e) {
                name = String.format("0x%8x", id);
            }
            idNames.put(id, name);
        }
        return name;
    }

    private static abstract class Pool<T> {
        private final Deque<T> pool;

        Pool(int initialSize) {
            pool = new ArrayDeque<>(initialSize);
            for (int i = 0; i < initialSize; i++) {
                pool.addLast(newObject());
            }
        }

        T obtain() {
            return pool.isEmpty() ? newObject() : pool.removeLast();
        }

        void restore(T instance) {
            pool.addLast(instance);
        }

        protected abstract T newObject();
    }
}
